
using Boilen.Guards;
using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Xunit;


namespace Boilen.Primitives.CodeGeneration {

    public static class Compile {

        public static MemberInfo[] PartialType( PartialType pt ) {
            var caller = GetCaller( );
            Type compiledType;
            return Compile.PartialType( caller, pt, out compiledType );
        }

        public static MemberInfo[] PartialType( PartialType pt, out Type compiledType ) {
            var caller = GetCaller( );
            return Compile.PartialType( caller, pt, out compiledType );
        }

        public static bool PartialType( PartialType pt, bool swallowRunErrors, out CompilerResults compileResults ) {
            var caller = GetCaller( );
            bool silverlight;
            return Compile.PartialType( caller, pt, swallowRunErrors, out silverlight, out compileResults );
        }


        private static MemberInfo[] PartialType( MethodBase caller, PartialType pt, out Type compiledType ) {
            bool silverlight;
            CompilerResults results;
            Compile.PartialType( caller, pt, false, out silverlight, out results );

            var errors = results.Errors.Cast<CompilerError>( );
            if( errors.Any( ) ) {
                string errorMessage = errors.Aggregate(
                    new System.Text.StringBuilder( ).AppendLine( silverlight ? "SL" : "WPF" ),
                    ( sb, e ) => sb.AppendLine( e.ToString( ) ),
                    ( sb ) => sb.ToString( )
                );
                Assert.True( false, errorMessage );
            }

            Type[] types = results.CompiledAssembly.GetExportedTypes( );
            compiledType = Assert.Single( types );
            Assert.Equal( compiledType.FullName, pt.Type.FullName );

            var compiledTypeLocal = compiledType;
            var newMembers = compiledType.GetMembers( )
                .Where( m => m.DeclaringType == compiledTypeLocal )
                .ToArray( );

            return newMembers;
        }

        private static bool PartialType( MethodBase caller, PartialType pt, bool swallowRunErrors, out bool silverlight, out CompilerResults compileResults ) {
            var writer = new CodeFileWriter( caller, pt.Type );

            bool runResult = true;
            using( writer ) {
                if( swallowRunErrors ) {
                    try { pt.Run( writer ); }
                    catch { runResult = false; }
                }
                else
                    pt.Run( writer );
            }

            string generatedFile = writer.FilePath;
            string sourceFile = Path.Combine( CodeFileWriter.CodeDirectory, writer.TypeName + ".cs" );
            silverlight = true;
            compileResults = GetCompileResults( silverlight, generatedFile, sourceFile );
            if( compileResults.Errors.Count == 0 ) {
                silverlight = false;
                compileResults = GetCompileResults( silverlight, generatedFile, sourceFile );
            }

            return runResult;
        }

        private static CompilerResults GetCompileResults( bool silverlight, params string[] files ) {
            string[] referenceAssemblies = (silverlight ? GetSilverlightReferences( ) : GetWpfReferences( )).ToArray( );

            var providerOptions = new Dictionary<string, string>( ) { { "CompilerVersion", "v4.0" } };
            var provider = new CSharpCodeProvider( providerOptions );
            var options = new CompilerParameters( ) { GenerateInMemory = true, TreatWarningsAsErrors = true, WarningLevel = 4 };
            options.TempFiles.KeepFiles = false;
            options.ReferencedAssemblies.AddRange( referenceAssemblies );
            if( silverlight ) { options.CompilerOptions = "/nostdlib+ /define:" + CompilationSymbol.Silverlight.Symbol; }

            return provider.CompileAssemblyFromFile( options, files );
        }


        private static readonly string[] excludedAssemblies = new string[] {
            typeof( int ).Assembly.FullName,
            typeof( Compile ).Assembly.FullName,
            typeof( PartialType ).Assembly.FullName,
            typeof( Xunit.Assert ).Assembly.FullName,
            typeof( Xunit.Extensions.TheoryAttribute ).Assembly.FullName,
        };

        private static IEnumerable<string> GetWpfReferences( ) {
            return AppDomain.CurrentDomain.GetAssemblies( )
                .Where( a => !string.IsNullOrEmpty( a.Location )
                    && !a.FullName.StartsWith( "Microsoft." )
                    && !excludedAssemblies.Contains( a.FullName ) )
                .Select( a => a.Location );
        }

        private static IEnumerable<string> GetSilverlightReferences( ) {
            string wpfSourceDependenciesFile = new Uri( typeof( TestGuards ).Assembly.CodeBase, UriKind.Absolute ).AbsolutePath;
            string sourceDependenciesName = Path.GetFileNameWithoutExtension( wpfSourceDependenciesFile );
            var sourceDependenciesDirectory = Directory.GetParent( wpfSourceDependenciesFile );
            while( sourceDependenciesDirectory.GetDirectories( sourceDependenciesName ).Length == 0 )
                sourceDependenciesDirectory = sourceDependenciesDirectory.Parent;
            string slSourceDependenciesFile = new[] { 
                sourceDependenciesDirectory.FullName, 
                sourceDependenciesName, "bin", "Debug", sourceDependenciesName + ".SL.dll"
            }.Aggregate( Path.Combine );

            string slReferencesDirectory = new[] {
                Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ),
                "Reference Assemblies", "Microsoft", "Framework", "Silverlight", "v4.0"
            }.Aggregate( Path.Combine );
            string[] slReferences = Directory.GetFiles( slReferencesDirectory, "*.dll", SearchOption.TopDirectoryOnly );

            return slReferences
                .Where( path => {
                    string fileName = Path.GetFileNameWithoutExtension( path );
                    bool include = fileName.Any( char.IsUpper ) && fileName != "Silverlight.ConfigurationUI";
                    return fileName == "system" || fileName == "mscorlib" || include;
                } )
                .Concat( new[] { slSourceDependenciesFile } );
        }


        private static MethodBase GetCaller( ) {
            return new StackTrace( ).GetFrame( 2 ).GetMethod( );
        }

    }

}
